﻿/*  PandoraUnleashed
    An open source API for the Pandora music service
    Copyright © 2011 Saulius Milevicius

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using PandoraUnleashed.Data;
using PandoraUnleashed.Utils;

namespace PandoraUnleashed
{
    class Pandora
    {
        private double adRefreshInterval, videoAdUniqueInterval, videoAdRefreshInterval, videoAdStartInterval;
        private int audioAdIndex = 1;
        private int adLogonSponsorIndex = 1;
        private string stationSkipUnit;
        private long syncTime = 0;
        private String proxy;
        private int proxyTimeout;
        private String fw_ver = "2.3.4";
        private String version = "1.5.13";
        public String adLogonSponsor;
        public String genreStationsChecksum;
        public UserData myData;
        public List<GenreData> myGenreStations;
        public PartnerData myPartnerData;
        public List<StationData> myStations;
        public String stationsChecksum;

        /// <summary>
        /// Default constructor for the Pandora class
        /// </summary>
        /// <param name="version">Specify a specific version of the Pandora app</param>
        /// <param name="fwVersion">Specify a specific Android firmware version</param>
        public Pandora(String version = "1.5.13", String fwVersion = "2.3.4")
        {
            myStations = new List<StationData>();
            myGenreStations = new List<GenreData>();
            this.version = version;
            this.fw_ver = fwVersion;
        }

        /// <summary>
        /// Check whether the Pandora service is available in your area
        /// </summary>
        /// <returns>Boolean representing whether you are allowed to use the service</returns>
        public Boolean checkLicensing()
        {
            String result = execute("test.checkLicensing", new Hashtable(), null, 0);
            return (Boolean)((Hashtable)((Hashtable)JSON.JsonDecode(result))["result"])["isAllowed"];
        }

        /// <summary>
        /// Check whether there is an upgrade for your version
        /// </summary>
        /// <returns>True if there is an update</returns>
        public bool checkForUpgrade()
        {
            String result = executeGetRequest("http://tuner.pandora.com/services/ajax/?method=mobile.checkForUpgrade&type=full&version=" + version + "&vnumber=hero&appName=pandora&deviceId=0");
            Hashtable table = (Hashtable)((Hashtable)JSON.JsonDecode(result))["result"];
            return (Boolean)table["upgradeRequired"];
        }

        /// <summary>
        /// Logon to Pandoras service as an Android partner
        /// </summary>
        /// <returns>True if successful</returns>
        private bool partnerLogin()
        {
            byte[] PP = { 65, 67, 55, 73, 66, 71, 48, 57, 65, 51, 68, 84, 83, 89, 77, 52, 82, 52, 49, 85, 74, 87, 76, 48, 55, 86, 76, 78, 56, 74, 73, 55 };

            Hashtable body = new Hashtable();
            body.Add("username", "android");
            body.Add("password", Encoding.UTF8.GetString(PP));
            body.Add("version", "5");
            body.Add("deviceModel", "hero");
            body.Add("includeUrls", true);
            String result = executeSecure("auth.partnerLogin", body, null, 0);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if ((String)table["stat"] == "ok")
            {
                table = (Hashtable)table["result"];
                String partnerId = (String)table["partnerId"];
                String partnerAuthToken = (String)table["partnerAuthToken"];
                stationSkipUnit = (String)table["stationSkipUnit"];
                Hashtable urls = (Hashtable)table["urls"];
                String autoComplete = (String)urls["autoComplete"];
                Double stationSkipLimit = (double)table["stationSkipLimit"];
                myPartnerData = new PartnerData(autoComplete, partnerAuthToken, partnerId, (int)stationSkipLimit);

                Hashtable deviceProperties = (Hashtable)table["deviceProperties"];
                adRefreshInterval = (double)deviceProperties["adRefreshInterval"];
                videoAdRefreshInterval = (double)deviceProperties["videoAdRefreshInterval"];
                videoAdStartInterval = (double)deviceProperties["videoAdStartInterval"];
                videoAdUniqueInterval = (double)deviceProperties["videoAdUniqueInterval"];
                String syncTimeStr = (String)table["syncTime"];
                syncTime = long.Parse(decryptSyncTime(syncTimeStr));
                TimeSpan ts = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0));
                syncTime = ((long)ts.TotalMilliseconds - 1000 * syncTime);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Use a proxy to communicate with the service
        /// </summary>
        /// <param name="proxy">Proxy string in IP:Port format</param>
        /// <param name="proxyTimeout">Timeout in milliseconds for requests</param>
        public void setProxy(String proxy, int proxyTimeout = 30000)
        {
            this.proxy = proxy;
            this.proxyTimeout = proxyTimeout;
        }

        /// <summary>
        /// Check whether a proxy is set
        /// </summary>
        /// <returns>True if a proxy is being used</returns>
        public bool isProxySet()
        {
            return (proxy == null);
        }

        /// <summary>
        /// Remove the proxy
        /// </summary>
        public void removeProxy()
        {
            proxy = null;
        }

        /// <summary>
        /// Login as a partner administrator
        /// </summary>
        /// <returns>True if successful</returns>
        public bool partnerAdminLogin()
        {
            byte[] PP = { 65, 67, 55, 73, 66, 71, 48, 57, 65, 51, 68, 84, 83, 89, 77, 52, 82, 52, 49, 85, 74, 87, 76, 48, 55, 86, 76, 78, 56, 74, 73, 55 };

            Hashtable body = new Hashtable();
            body.Add("username", "android");
            body.Add("password", Encoding.UTF8.GetString(PP));
            body.Add("version", "5");
            String result = executeSecure("auth.partnerAdminLogin", body, null, 0);
            if (result.Contains("stat\":\"ok"))
            {
                Hashtable table = (Hashtable)((Hashtable)JSON.JsonDecode(result))["result"];
                String partnerId = (String)table["partnerId"];
                String partnerAdminAuthToken = (String)table["partnerAdminAuthToken"];

                myPartnerData = new PartnerData(partnerAdminAuthToken, partnerId);

                String syncTimeStr = (String)table["syncTime"];
                syncTime = long.Parse(decryptSyncTime(syncTimeStr));
                TimeSpan ts = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0));
                syncTime = ((long)ts.TotalMilliseconds - 1000 * syncTime);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Create a new user for Pandora
        /// </summary>
        /// <param name="email">Desired email</param>
        /// <param name="password">Password (At least 5 chars)</param>
        /// <param name="gender">Gender ("male" or "female")</param>
        /// <param name="birthYear">Birth year (YYYY)</param>
        /// <param name="zipCode">5 digit zip code</param>
        /// <param name="emailOptIn">Opt in for personalized recommendations and tips via email</param>
        /// <returns>True if successful</returns>
        public bool createUser(String email, String password, String gender, int birthYear, String zipCode, Boolean emailOptIn)
        {
            if (password.Length < 5)
                return false;
            Hashtable body = new Hashtable();
            body.Add("username", email);
            body.Add("password", password);
            body.Add("gender", gender);
            body.Add("birthYear", birthYear);
            body.Add("zipCode", zipCode);
            body.Add("emailOptIn", emailOptIn);
            body.Add("countryCode", "US");
            body.Add("accountType", "registered");
            body.Add("registeredType", "user");
            body.Add("includePandoraOneInfo", true);

            String result = executeSecureEncrypted("user.createUser", body, null, 1);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if ((String)table["stat"] == "ok")
            {
                setUserFromResult(result);
                return true;
            }
            else
                return false;
        }

        /// <summary>
        /// Login to user account
        /// </summary>
        /// <param name="email">Email</param>
        /// <param name="password">Password</param>
        /// <returns>True if successful</returns>
        public bool userLogin(String email, String password)
        {
            Hashtable body = new Hashtable();
            body.Add("username", email);
            body.Add("password", password);
            body.Add("loginType", "user");
            addCommonUserLoginParams(body);
            body.Add("complimentarySponsorSupported", true);
            body.Add("includeSubscriptionExpiration", true);
            String result = executeSecureEncrypted("auth.userLogin", body, null, 1);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if ((String)table["stat"] == "ok")
            {
                setUserFromResult(result);
                return true;
            }
            else
                return false;
        }

        /// <summary>
        /// Login to an account with an associated device id
        /// </summary>
        /// <param name="deviceId">A unique id (guid)</param>
        /// <returns>True if successful</returns>
        public bool deviceLogin(String deviceId)
        {
            if (myPartnerData == null)
                throw new Exception("Not logged in as partner");
            Hashtable body = new Hashtable();
            body.Add("loginType", "deviceId");
            body.Add("deviceId", deviceId);
            addCommonUserLoginParams(body);
            body.Add("complimentarySponsorSupported", true);
            body.Add("includeSubscriptionExpiration", true);
            String result = executeSecureEncrypted("auth.userLogin", body, null, 1);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if ((String)table["stat"] == "ok")
            {
                setUserFromResult(result);
                return true;
            }
            else
                return false;
        }

        /// <summary>
        /// Create a new station
        /// </summary>
        /// <param name="postItems">Hashtable of params</param>
        /// <returns>The newly created stations StationData</returns>
        /// <remarks>Also adds the new station to the myStations list</remarks>
        private StationData createStationCommon(Hashtable postItems)
        {
            String result = executeEncrypted("station.createStation", postItems, null, 2);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if (table.ContainsKey("result"))
                table = (Hashtable)table["result"];
            StationData newStation = new StationData(table);
            myStations.Add(newStation);
            return newStation;
        }

        /// <summary>
        /// Get a checksum for the currently available genre stations
        /// </summary>
        /// <returns>Checksum string</returns>
        public String getGenreStationListChecksum()
        {
            String result = executeEncrypted("station.getGenreStationsChecksum", new Hashtable(), null, 2);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            return (String)table["checksum"];
        }

        /// <summary>
        /// Parse a hashtable of params into an AdData structure
        /// </summary>
        /// <param name="json">Hashtable of params</param>
        /// <returns>Ad data</returns>
        private AdData parseDisplayAd(Hashtable json)
        {
            if (json == null)
                return null;
            AdData adData = new AdData();
            adData.impressionUrl = (json.ContainsKey("impressionUrl")) ? (String)json["impressionUrl"] : "";
            adLogonSponsor = (json.ContainsKey("logonSponsor")) ? (String)json["logonSponsor"] : null;
            adData.html = (json.ContainsKey("html")) ? (String)json["html"] : "";
            adData.bannerRenderTrackerUrl = (json.ContainsKey("bannerRenderTracker")) ? (String)json["bannerRenderTracker"] : "";
            adData.bannerRenderDarkTrackerUrl = (json.ContainsKey("bannerRenderDarkTracker")) ? (String)json["bannerRenderDarkTracker"] : "";
            adData.bannerDisplayAfterDarkTrackerUrl = (json.ContainsKey("bannerDisplayAfterDarkTrackerUrl")) ? (String)json["bannerDisplayAfterDarkTrackerUrl"] : "";
            adData.height = (json.ContainsKey("height")) ? (int)json["height"] : 75;
            adData.clickUrl = (json.ContainsKey("clickUrl")) ? (String)json["clickUrl"] : "";
            adData.imageUrl = (json.ContainsKey("imageUrl")) ? (String)json["imageUrl"] : "";
            if (adData.html == "")
            {
                if (adData.clickUrl != "")
                    adData.html = "<html><head></head><body style=\"padding:0px;margin:0px;background-color:transparent;\"><a target=\"_blank\" href=\"" + adData.clickUrl + "\" border=0><img src=\"" + adData.imageUrl + "\" style=\"height:100%%;width:100%%;\"></a></body></html>";
                else
                    adData.html = "<html><head></head><body style=\"padding:0px;margin:0px;background-color:transparent;\"><img src=\"" + adData.imageUrl + "\" style=\"height:100%%;width:100%%;\"></body></html>";
            }
            return adData;
        }

        /// <summary>
        /// Register an ad from a list of tracking tokens
        /// </summary>
        /// <param name="trackingTokens">Array list of tracking tokens</param>
        public void registerAd(ArrayList trackingTokens)
        {
            if (trackingTokens != null && trackingTokens.Count > 0)
            {
                Hashtable body = new Hashtable();
                body.Add("adTrackingTokens", trackingTokens);
                executeEncrypted("ad.registerAd", body, null, 2);
            }
        }

        /// <summary>
        /// Request an ad from an url
        /// </summary>
        /// <param name="url">Url to the ad</param>
        /// <returns>Ad data structure</returns>
        public AdData requestAd(String url)
        {
            String result = executeDartRequest(url);
            if (result != "")
            {
                if (result.IndexOf('{') > 0 && result.IndexOf('}') > 0)
                {
                    String ad = result.Substring(result.IndexOf('{'));
                    Hashtable table = (Hashtable)JSON.JsonDecode(ad);
                    if (table.ContainsKey("impressionUrl"))
                    {
                        return parseDisplayAd(table);
                    }
                    else
                        recordBrokenAd("Mobile Banner", ad, "No impressionUrl in DART ad");
                }
                else
                    recordBrokenAd("Mobile Banner", result, "No DART ad available, or unexpected creative content");
            }
            return null;
        }
        
        /// <summary>
        /// Request an html version of an ad
        /// </summary>
        /// <param name="url">Url to the ad</param>
        /// <returns>Html version of the ad</returns>
        public String requestAdHtml(String url)
        {
            if (myData != null)
            {
                if (!myData.isAdSupported)
                    return String.Empty;
                if (url != "")
                {
                    String result = executeDartRequest(url);
                    if (result == "")
                        result = recordBrokenAd("Station Creation Banner", url, "");
                    return result;
                }
            }
            return String.Empty;
        }

        /// <summary>
        /// Request an ad impression
        /// </summary>
        /// <param name="url">Url of the ad</param>
        public void requestAdImpression(String url)
        {
            executeDartRequest(url);
        }

        /// <summary>
        /// Pandoras autocomplete search
        /// </summary>
        /// <param name="query">Search query</param>
        /// <returns>List of possible matches</returns>
        public List<AutoCompleteSearchData> autocompleteSearch(String query)
        {
            String result = executeGetRequest("http://autocomplete.pandora.com/s2?q=" + System.Web.HttpUtility.UrlEncode(query) + "&sendquery=no");
            String[] res = result.Split('\n');
            List<AutoCompleteSearchData> acsd = new List<AutoCompleteSearchData>();
            for (int i = 0; i < res.Length; ++i)
            {
                String[] tmp = res[i].Split('\t');
                if (tmp[0][0] == 'S')
                    acsd.Add(new AutoCompleteSearchData(tmp[0], tmp[2] + " by " + tmp[1], 2));
                else
                    acsd.Add(new AutoCompleteSearchData(tmp[0], tmp[1], resolveSeedType(tmp[0])));
            }
            return acsd;
        }

        /// <summary>
        /// Resolve the type of of data
        /// </summary>
        /// <param name="id">Music id</param>
        /// <returns>The type of music the id belongs to</returns>
        private int resolveSeedType(String id)
        {
            switch (id[0])
            {
                case 'C': return 0;
                case 'R': return 2;
                case 'G': return 3;
                default: return -1;
            }
        }

        /// <summary>
        /// Acknowledge the expiration of a Pandora One trial
        /// </summary>
        public void acknowledgeP1TrialExpiration()
        {
            executeEncrypted("user.acknowledgeSubscriptionExpiration", new Hashtable(), null, 2);
        }

        /// <summary>
        /// Bookmark an artist
        /// </summary>
        /// <param name="trackToken">Track token for the artist</param>
        public void addArtistBookmark(String trackToken)
        {
            Hashtable body = new Hashtable();
            body.Add("trackToken", trackToken);
            executeEncrypted("bookmark.addArtistBookmark", body, null, 2);
        }

        /// <summary>
        /// Add feedback to a track
        /// </summary>
        /// <param name="trackToken">Track token</param>
        /// <param name="isPositive">Is the feedback positive</param>
        /// <remarks><paramref name="isPositive"/> stands for thumbs up/down. True for thumbs up, False for thumbs down.</remarks>
        public void addFeedback(String trackToken, Boolean isPositive)
        {
            Hashtable body = new Hashtable();
            body.Add("trackToken", trackToken);
            body.Add("isPositive", isPositive);
            executeEncrypted("station.addFeedback", body, null, 2);
        }

        /// <summary>
        /// Bookmark a song
        /// </summary>
        /// <param name="trackToken">The songs track token</param>
        public void addSongBookmark(String trackToken)
        {
            Hashtable body = new Hashtable();
            body.Add("trackToken", trackToken);
            executeEncrypted("bookmark.addSongBookmark", body, null, 2);
        }

        /// <summary>
        /// Associate a unique id to your account to be used with deviceLogin(String)
        /// </summary>
        /// <param name="deviceId">The unique id you want to associate to the account</param>
        /// <remarks>You must be logged into a user account to associate an id</remarks>
        public void associateDevice(String deviceId)
        {
            Hashtable body = new Hashtable();
            body.Add("deviceId", deviceId);
            executeSecureEncrypted("user.associateDevice", body, null, 2);
        }

        /// <summary>
        /// Add a new device model to the Pandora service
        /// </summary>
        /// <param name="deviceModel">Name of the model</param>
        /// <param name="description">Description of the model</param>
        public void createDevice(String deviceModel, String description)
        {
            Hashtable body = new Hashtable();
            body.Add("partnerAdminAuthToken", myPartnerData.partnerAuthToken);
            body.Add("deviceModel", deviceModel);
            body.Add("description", description);
            executeSecureEncrypted("device.createDevice", body, null, 3);
        }

        /// <summary>
        /// Create a station from a music token
        /// </summary>
        /// <param name="musicToken">Music token with which to create the station</param>
        /// <returns>The station data of the new station</returns>
        /// <remarks>The station is automatically added to the myStations list and will be loaded into there each time you login until you delete it</remarks>
        public StationData createStationFromMusicToken(String musicToken)
        {
            Hashtable body = new Hashtable();
            body.Add("musicToken", musicToken);
            body.Add("includeStationArtUrl", true);
            body.Add("stationArtSize", "W176H220");
            body.Add("includeAdAttributes", true);
            return createStationCommon(body);
        }

        /// <summary>
        /// Create a station from a track token
        /// </summary>
        /// <param name="trackToken">The track token with which to create the station</param>
        /// <param name="musicType">The type of music</param>
        /// <returns>The station data of the new station</returns>
        /// <remarks>The station is automatically added to the myStations list and will be loaded into there each time you login until you delete it</remarks>
        public StationData createStationFromTrackToken(String trackToken, String musicType)
        {
            Hashtable body = new Hashtable();
            body.Add("trackToken", trackToken);
            body.Add("musicType", musicType);
            body.Add("includeStationArtUrl", true);
            body.Add("stationArtSize", "W176H220");
            body.Add("includeAdAttributes", true);
            return createStationCommon(body);
        }

        /// <summary>
        /// Delete a station from your account
        /// </summary>
        /// <param name="stationToken">The stations token that you want removed</param>
        /// <remarks>The station is also removed from the myStations list and will not be loaded anymore when you sign in</remarks>
        public void deleteStation(String stationToken)
        {
            Hashtable body = new Hashtable();
            body.Add("stationToken", stationToken);
            executeEncrypted("station.deleteStation", body, null, 2);
            for (int i = 0; i < myStations.Count; ++i)
            {
                if (myStations[i].stationToken == stationToken)
                {
                    myStations.RemoveAt(i);
                    break;
                }
            }
        }

        /// <summary>
        /// Disassociate a unique id from your account
        /// </summary>
        /// <param name="deviceId">The unique id you want disassociated from the account</param>
        /// <remarks>You must be signed in to the account, and the id has to be associated with your account.</remarks>
        public void disassociateDevice(String deviceId)
        {
            Hashtable body = new Hashtable();
            body.Add("deviceId", deviceId);
            executeSecureEncrypted("device.disassociateDevice", body, null, 1);
        }

        /// <summary>
        /// Email a password reminder
        /// </summary>
        /// <param name="email">The email of the account</param>
        public void emailPassword(String email)
        {
            Hashtable body = new Hashtable();
            body.Add("username", email);
            executeSecureEncrypted("user.emailPassword", body, null, 1);
        }

        /// <summary>
        /// Explain a track
        /// </summary>
        /// <param name="trackToken">The track token of the track you want explained</param>
        /// <returns></returns>
        public String explainTrack(String trackToken)
        {
            Hashtable body = new Hashtable();
            body.Add("trackToken", trackToken);
            String result = executeEncrypted("track.explainTrack", body, null, 2);
            return result;
        }

        /// <summary>
        /// Get the metadata of an ad
        /// </summary>
        /// <param name="audioAd">The audio ad you want to get metadata for</param>
        /// <param name="currentStation">The station the ad appeared in</param>
        /// <returns>Returns the passed in audioAd variable with the updated metadata</returns>
        public AudioAdData getAdMetadata(AudioAdData audioAd, StationData currentStation)
        {
            if (audioAd == null || audioAd.adToken == "")
                return audioAd;
            Hashtable body = new Hashtable();
            body.Add("adToken", audioAd.adToken);
            body.Add("returnAdTrackingTokens", true);
            body.Add("supportAudioAds", true);
            body.Add("includeBannerAd", true);
            Dictionary<String, String> urlParams = new Dictionary<string, string>();
            if (currentStation.supportImpressionTargeting)
                urlParams.Add("audioAdIndex", audioAdIndex++.ToString());
            String result = executeEncrypted("ad.getAdMetadata", body, urlParams, 2);
            Hashtable table = (Hashtable)((Hashtable)JSON.JsonDecode(result))["result"];
            AdData adData = null;
            if (table.ContainsKey("bannerAdMap"))
            {
                adData = parseDisplayAd((Hashtable)table["bannerAdMap"]);
                adData.isAudioAdFollowOnBanner = true;
                adData.height = 50;
            }
            audioAd.updateAdMetadata(table, (table.ContainsKey("audioUrlMap") ? buildAudioUrlMap((Hashtable)table["audioUrlMap"]) : null), adData);
            return audioAd;
        }

        /// <summary>
        /// Get an artists info
        /// </summary>
        /// <param name="url">artistExplorerUrl of the artist you want info for</param>
        /// <returns>ArtistInfo structure of the info</returns>
        public ArtistInfo getArtistInfo(String url)
        {
            String result = executeGetRequest(url.Replace("/xml/", "/json/").Replace("/beta.savagebeast.com:7001/", "/www.pandora.com/"));
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if (table.ContainsKey("artistExplorer"))
                return new ArtistInfo((Hashtable)table["artistExplorer"]);
            if (table.ContainsKey("composerExplorer"))
                return new ArtistInfo((Hashtable)table["composerExplorer"]);
            return null;
        }

        /// <summary>
        /// Get the current genre stations that are available
        /// </summary>
        /// <remarks>These stations will be put into the myGenreStations list</remarks>
        public void getGenreStations()
        {
            if (genreStationsChecksum == getGenreStationListChecksum())
                return;
            Hashtable body = new Hashtable();
            body.Add("includeChecksum", true);
            body.Add("includeStationArtUrl", true);
            String result = executeEncrypted("station.getGenreStations", body, null, 2);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if (table.ContainsKey("categories"))
            {
                myGenreStations = new List<GenreData>();
                ArrayList list = (ArrayList)table["categories"];
                for (int i = 0; i < list.Count; ++i)
                    myGenreStations.Add(new GenreData((Hashtable)list[i]));
            }
        }

        /// <summary>
        /// Get a stations playlist
        /// </summary>
        /// <param name="stationData">The StationData of the station you are listening to</param>
        /// <returns>A list of tracks for the supplied station</returns>
        public List<TrackData> getPlaylist(StationData stationData)
        {
            Hashtable body = new Hashtable();
            body.Add("stationToken", stationData.stationToken);
            body.Add("inlcudeTrackLength", true);
            body.Add("includeDeviceProperties", true);
            String result = executeSecureEncrypted("station.getPlaylist", body, null, 2);
            Hashtable table = (Hashtable)((Hashtable)JSON.JsonDecode(result))["result"];
            ArrayList list = (ArrayList)table["items"];
            List<TrackData> trackData = new List<TrackData>();
            for (int i = 0; i < list.Count; ++i)
            {
                Hashtable temp = (Hashtable)list[i];
                if (temp.ContainsKey("adToken"))
                    trackData.Add(new AudioAdData(long.Parse(stationData.stationId), (String)temp["adToken"]));
                if (temp.ContainsKey("amazonSongDigitalAsin"))
                {
                    String nowPlayingStationAdUrl = ((temp.ContainsKey("nowPlayingStationAdUrl")) ? (String)temp["nowPlayingStationAdUrl"] : "");
                    String trackGain = ((temp.ContainsKey("trackGain")) ? (String)temp["trackGain"] : "");
                    String itunesSongUrl = ((temp.ContainsKey("itunesSongUrl")) ? (String)temp["itunesSongUrl"] : "");
                    String amazonAlbumUrl = ((temp.ContainsKey("amazonAlbumUrl")) ? (String)temp["amazonAlbumUrl"] : "");
                    String amazonAlbumDigitalAsin = ((temp.ContainsKey("amazonAlbumDigitalAsin")) ? (String)temp["amazonAlbumDigitalAsin"] : "");
                    String artistDetailUrl = ((temp.ContainsKey("artistDetailUrl")) ? (String)temp["artistDetailUrl"] : "");
                    String albumDetailUrl = ((temp.ContainsKey("albumDetailUrl")) ? (String)temp["albumDetailUrl"] : "");
                    String songDetailUrl = ((temp.ContainsKey("songDetailUrl")) ? (String)temp["songDetailUrl"] : "");
                    String amazonAlbumAsin = ((temp.ContainsKey("amazonAlbumAsin")) ? (String)temp["amazonAlbumAsin"] : "");
                    String albumExplorerUrl = ((temp.ContainsKey("albumExplorerUrl")) ? (String)temp["albumExplorerUrl"] : "");
                    String songExplorerUrl = ((temp.ContainsKey("songExplorerUrl")) ? (String)temp["songExplorerUrl"] : "");
                    trackData.Add(new TrackData(long.Parse(stationData.stationId), (String)temp["songName"], (String)temp["artistName"], (String)temp["albumName"], (String)temp["albumArtUrl"], (String)temp["trackToken"], nowPlayingStationAdUrl, (Boolean)temp["allowFeedback"], (int)(double)temp["songRating"], trackGain, buildAudioUrlMap((Hashtable)temp["audioUrlMap"]), (String)temp["amazonSongDigitalAsin"], (String)temp["artistExplorerUrl"], itunesSongUrl, amazonAlbumUrl, amazonAlbumDigitalAsin, artistDetailUrl, albumDetailUrl, songDetailUrl, amazonAlbumAsin, albumExplorerUrl, songExplorerUrl));
                }
            }
            return trackData;
        }

        /// <summary>
        /// Build an url map from a hashtable of params
        /// </summary>
        /// <param name="json">Hashtable of params</param>
        /// <returns>Audio url map</returns>
        private Dictionary<String, Dictionary<String, String>> buildAudioUrlMap(Hashtable json)
        {
            Dictionary<String, Dictionary<String, String>> ret = new Dictionary<string, Dictionary<string, string>>();
            String[] audioQuality = { "highQuality", "mediumQuality", "lowQuality" };
            for (int i = 0; i < 3; ++i)
            {
                String quality = audioQuality[i];
                if (json.ContainsKey(quality))
                {
                    Hashtable table = (Hashtable)json[quality];
                    String[] audioAttributes = { "audioUrl", "bitrate", "encoding" };
                    Dictionary<String, String> attributes = new Dictionary<string, string>();
                    for (int j = 0; j < 3; ++j)
                    {
                        attributes.Add(audioAttributes[j], (String)table[audioAttributes[j]]);
                    }
                    ret.Add(quality, attributes);
                }
            }
            return ret;
        }

        /// <summary>
        /// Get the users station list
        /// </summary>
        /// <remarks>The list gets put into the myStations list</remarks>
        public void getStationList()
        {
            if (stationsChecksum == getStationListChecksum())
                return;
            Hashtable body = new Hashtable();
            body.Add("includeStationArtUrl", true);
            body.Add("stationArtSize", "W176H220");
            body.Add("includeAdAttributes", true);
            String result = executeEncrypted("user.getStationList", body, null, 2);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            if (table.ContainsKey("stations"))
            {
                myStations = new List<StationData>();
                ArrayList list = (ArrayList)table["stations"];
                for (int i = 0; i < list.Count; ++i)
                    myStations.Add(new StationData((Hashtable)list[i]));
            }
        }

        /// <summary>
        /// Get the checksum for the users station list
        /// </summary>
        /// <returns>The current checksum of the users station list</returns>
        public String getStationListChecksum()
        {
            return executeEncrypted("user.getStationListChecksum", new Hashtable(), null, 2);
        }

        /// <summary>
        /// Search Pandoras collection
        /// </summary>
        /// <param name="searchText">The search query</param>
        /// <param name="returnExactMatch">Whether you want this search to return an exact match</param>
        /// <returns>The search data for the query</returns>
        /// <remarks>If <paramref name="returnExactMatch"/> is true, if a match has a score of 100 the returned search data will have the <paramref name="exactMatchToken"/> variable populated instead of the lists.</remarks>
        public MusicSearchData searchMusic(String searchText, Boolean returnExactMatch)
        {
            Hashtable body = new Hashtable();
            body.Add("searchText", searchText);
            body.Add("includeNearMatches", true);
            String result = executeEncrypted("music.search", body, null, 2);
            Hashtable table = (Hashtable)(((Hashtable)JSON.JsonDecode(result))["result"]);
            ArrayList artists = (ArrayList)table["artists"];
            List<ArtistSearchData> artistSearchData = new List<ArtistSearchData>();
            String token = null;
            for (int i = 0; i < artists.Count; ++i)
            {
                ArtistSearchData asd = new ArtistSearchData((Hashtable)artists[i]);
                artistSearchData.Add(asd);
                if (returnExactMatch)
                {
                    if (asd.score == 100)
                        token = asd.musicToken;
                }
            }
            ArrayList songs = (ArrayList)table["songs"];
            List<SongSearchData> songSearchData = new List<SongSearchData>();
            for (int i = 0; i < songs.Count; ++i)
            {
                SongSearchData ssd = new SongSearchData((Hashtable)songs[i]);
                songSearchData.Add(ssd);
                if (returnExactMatch)
                {
                    if (ssd.score == 100)
                        token = ssd.musicToken;
                }
            }
            if (token != null)
                return new MusicSearchData(token);
            else
                return new MusicSearchData(artistSearchData, songSearchData);
        }

        /// <summary>
        /// Re-authenticate with the service with your unique id
        /// </summary>
        /// <param name="deviceId">The unique id associated with the account</param>
        public void reAuth(String deviceId)
        {
            deviceLogin(deviceId);
        }

        /// <summary>
        /// Toggle the privacy of the accounts profile
        /// </summary>
        /// <param name="makeProfilePrivate">Whether to make the users account private or not</param>
        public void setAwareOfProfile(Boolean makeProfilePrivate)
        {
            Hashtable body = new Hashtable();
            body.Add("makeProfilePrivate", makeProfilePrivate);
            executeEncrypted("user.setAwareOfProfile", body, null, 2);
        }

        /// <summary>
        /// Share a station with a list of emails
        /// </summary>
        /// <param name="stationIdOrToken">The station id or token</param>
        /// <param name="emails">An ArrayList of emails you want to share the station with</param>
        public void shareStation(String stationIdOrToken, ArrayList emails)
        {
            Hashtable body = new Hashtable();
            body.Add("stationId", stationIdOrToken);
            body.Add("stationToken", stationIdOrToken);
            body.Add("emails", emails);
            executeEncrypted("station.shareStation", body, null, 2);
        }

        /// <summary>
        /// Start a Pandora One trial on the users account
        /// </summary>
        /// <param name="sponsorName">The name of the sponsor of the trial</param>
        /// <returns>Return sponsorship data for the trial</returns>
        public SponsorshipData startP1Trial(String sponsorName)
        {
            Hashtable body = new Hashtable();
            body.Add("complimentarySponsor", sponsorName);
            String result = executeEncrypted("user.startComplimentaryTrial", body, null, 2);
            Hashtable table = (Hashtable)JSON.JsonDecode(result);
            return new SponsorshipData(table);
        }

        /// <summary>
        /// Record a broken ad
        /// </summary>
        /// <param name="adType">The type of ad it was</param>
        /// <param name="ad">The ad itself</param>
        /// <param name="reason">Why was the ad broken</param>
        /// <returns>Response of the server</returns>
        private String recordBrokenAd(String adType, String ad, String reason)
        {
            String url = "http://www.pandora.com/radio/services/brokenAd.jsp?" +
                         "adUrl=" + System.Web.HttpUtility.UrlEncode(ad) +
                         "&adType=" + System.Web.HttpUtility.UrlEncode(adType) +
                         "&client=" + System.Web.HttpUtility.UrlEncode("Pandora/" + version + " Android/" + fw_ver + " generic") +
                         "&reason=" + System.Web.HttpUtility.UrlEncode(reason);
            return executeGetRequest(url);
        }

        /// <summary>
        /// Execute a call to the Pandora service
        /// </summary>
        /// <param name="method">The method we are executing</param>
        /// <param name="postItems">A hashtable of json params we will be sending</param>
        /// <param name="urlParams">Any extra url params</param>
        /// <param name="type">The type of call to make</param>
        /// <returns>The response of the service</returns>
        private String execute(String method, Hashtable postItems, Dictionary<String, String> urlParams, int type)
        {
            return innerExecute(buildUrl(method, false, postItems, urlParams, type), method, postItems, urlParams, false);
        }

        /// <summary>
        /// Execute a dart request
        /// </summary>
        /// <param name="url">The url of the request</param>
        /// <returns>The response of the request</returns>
        private String executeDartRequest(String url)
        {
            TimeSpan ts = (DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0));
            if (url.Contains("__CACHEBUST__"))
                url = url.Replace("__CACHEBUST__", ((long)ts.TotalMilliseconds).ToString());
            if (url.Contains("__LOGON__") && adLogonSponsor != null)
                url = url.Replace("__LOGON__", adLogonSponsor);
            if (url.Contains("__INDEX__"))
            {
                url = url.Replace("__INDEX__", adLogonSponsorIndex.ToString());
                adLogonSponsorIndex++;
            }

            String result = executeGetRequest(url);
            return result;
        }

        /// <summary>
        /// Execute a secure call to the pandora service
        /// </summary>
        /// <param name="method">The method we are executing</param>
        /// <param name="postItems">A hashtable of json params we will be sending</param>
        /// <param name="urlParams">Any extra url params</param>
        /// <param name="type">The type of call to make</param>
        /// <returns>The response of the service</returns>
        private String executeSecure(String method, Hashtable postItems, Dictionary<String, String> urlParams, int type)
        {
            return innerExecute(buildUrl(method, true, postItems, urlParams, type), method, postItems, urlParams, false);
        }

        /// <summary>
        /// Execute an encrypted call to the Pandora service
        /// </summary>
        /// <param name="method">The method we are executing</param>
        /// <param name="postItems">A hashtable of json params we will be sending</param>
        /// <param name="urlParams">Any extra url params</param>
        /// <param name="type">The type of call to make</param>
        /// <returns>The response of the service</returns>
        private String executeEncrypted(String method, Hashtable postItems, Dictionary<String, String> urlParams, int type)
        {
            return innerExecute(buildUrl(method, false, postItems, urlParams, type), method, postItems, urlParams, true);
        }

        /// <summary>
        /// Execute a secure and encrypted call to the Pandora service
        /// </summary>
        /// <param name="method">The method we are executing</param>
        /// <param name="postItems">A hashtable of json params we will be sending</param>
        /// <param name="urlParams">Any extra url params</param>
        /// <param name="type">The type of call to make</param>
        /// <returns>The response of the service</returns>
        private String executeSecureEncrypted(String method, Hashtable postItems, Dictionary<String, String> urlParams, int type)
        {
            return innerExecute(buildUrl(method, true, postItems, urlParams, type), method, postItems, urlParams, true);
        }

        /// <summary>
        /// Build the url for a call to the Pandora service
        /// </summary>
        /// <param name="method">The method we are executing</param>
        /// <param name="secure">Is this a secure call to the service</param>
        /// <param name="postItems">A hashtable of json params we will be sending</param>
        /// <param name="urlParams">Any extra url params</param>
        /// <param name="type">The type of call to make</param>
        /// <returns>The built url</returns>
        private String buildUrl(String method, Boolean secure, Hashtable postItems, Dictionary<String, String> urlParams, int type)
        {
            String url = "";
            if (secure)
                url = "https://tuner.pandora.com/services/json/";
            else
                url = "http://tuner.pandora.com/services/json/";
            url += "?method=" + method;
            if (urlParams != null)
            {
                for (int i = 0; i < urlParams.Count; ++i)
                    url += "&" + urlParams.Keys.ElementAt(i) + "=" + System.Web.HttpUtility.UrlEncode(urlParams.Values.ElementAt(i));
            }
            String token = null;
            String id1 = null;
            String id2 = null;

            if (type == 1 && myPartnerData != null)
            {
                token = myPartnerData.partnerAuthToken;
                id1 = myPartnerData.partnerId;
                postItems.Add("partnerAuthToken", myPartnerData.partnerAuthToken);
            }
            else if (type == 2 && myData != null)
            {
                token = myData.userAuthToken;
                id1 = myPartnerData.partnerId;
                id2 = myData.userId;
                postItems.Add("userAuthToken", myData.userAuthToken);
            }
            else if (type == 3 && myPartnerData != null)
            {
                token = myPartnerData.partnerAuthToken;
                id1 = myPartnerData.partnerId;
                postItems.Add("partnerAdminAuthToken", myPartnerData.partnerAuthToken);
            }
            if (token != null)
                url += "&auth_token=" + System.Web.HttpUtility.UrlEncode(token);
            if (id1 != null)
                url += "&partner_id=" + id1;
            if (id2 != null)
                url += "&user_id=" + id2;

            Hashtable deviceProperties = new Hashtable();
            deviceProperties.Add("applicationVersion", version);
            deviceProperties.Add("model", "android-hero");
            deviceProperties.Add("systemVersion", fw_ver);
            deviceProperties.Add("carrierName", "Android");
            deviceProperties.Add("w", "320");
            deviceProperties.Add("h", "480");
            postItems.Add("deviceProperties", deviceProperties);
            return url;
        }

        /// <summary>
        /// Execute a simple get request
        /// </summary>
        /// <param name="url">The url of the request</param>
        /// <returns>The response of the request</returns>
        private String executeGetRequest(String url)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            if (proxy != null)
            {
                WebProxy wp = new WebProxy(proxy.Split(':')[0], int.Parse(proxy.Split(':')[1]));
                request.Proxy = wp;
                request.Timeout = proxyTimeout;
            }
            request.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
            request.UserAgent = "Pandora/" + version + " Android/" + fw_ver + " generic";
            String result = String.Empty;
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                using (Stream responseStream = response.GetResponseStream())
                {
                    using (StreamReader readStream = new StreamReader(responseStream, Encoding.UTF8))
                    {
                        result = readStream.ReadToEnd();
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Execute a call to the Pandora service
        /// </summary>
        /// <param name="url">The url for the call</param>
        /// <param name="method">The method we are executing</param>
        /// <param name="postItems">A hashtable of json params we will be sending</param>
        /// <param name="urlParams">Any extra url params</param>
        /// <param name="encrypted">Is the call encrypted</param>
        /// <returns>The response of the service</returns>
        private String innerExecute(String url, String method, Hashtable postItems, Dictionary<String, String> urlParams, bool encrypted)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.UserAgent = "Pandora/" + version + " Android/" + fw_ver + " generic";
            if (proxy != null)
            {
                WebProxy wp = new WebProxy(proxy.Split(':')[0], int.Parse(proxy.Split(':')[1]));
                request.Proxy = wp;
                request.Timeout = proxyTimeout;
            }
            String body = JSON.tableToString(postItems).Replace(", ", ",");
            if (encrypted)
            {
                TimeSpan ts = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0));
                long unixTime = (long)ts.TotalMilliseconds;
                unixTime -= syncTime;
                unixTime /= 1000;
                postItems.Add("syncTime", unixTime);
                body = JSON.tableToString(postItems).Replace(", ", ",");
                body = body.Replace("\\", "");
                if (body.Length % 8 != 0)
                {
                    for (int i = 0; i < body.Length % 8; ++i)
                        body += " ";
                }

                byte[] unEnc = Encoding.ASCII.GetBytes(body);
                Blowfish blowfish = new Blowfish(new byte[] { 54, 35, 50, 54, 70, 82, 76, 36, 90, 87, 68 });
                byte[] enc = blowfish.Encrypt_ECB(unEnc);
                body = BitConverter.ToString(enc).Replace("-", "").ToLower();
            }
            request.ContentLength = body.Length;
            using (Stream writeStream = request.GetRequestStream())
            {
                UTF8Encoding encoding = new UTF8Encoding();
                byte[] bytes = encoding.GetBytes(body);
                writeStream.Write(bytes, 0, bytes.Length);
            }
            String result = String.Empty;
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                using (Stream responseStream = response.GetResponseStream())
                {
                    using (StreamReader readStream = new StreamReader(responseStream, Encoding.UTF8))
                    {
                        result = readStream.ReadToEnd();
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Decrypt the servers sync time
        /// </summary>
        /// <param name="syncTime">The encrypted sync time string</param>
        /// <returns>The decrypted sync time</returns>
        private String decryptSyncTime(String syncTime)
        {
            Blowfish blowfish = new Blowfish(new byte[] { 82, 61, 85, 33, 76, 72, 36, 79, 50, 66, 35 });
            byte[] st = decodeHex(syncTime);
            byte[] decrypted = blowfish.Decrypt_ECB(st);
            return Encoding.ASCII.GetString(decrypted).Substring(4, decrypted.Length - 6);
        }

        /// <summary>
        /// Add common login parameters to the hashtable
        /// </summary>
        /// <param name="postItems">A hashtable of json params we will be sending</param>
        private void addCommonUserLoginParams(Hashtable postItems)
        {
            postItems.Add("includePandoraOneInfo", true);
            postItems.Add("includeDemographics", true);
            postItems.Add("includeAdAttributes", true);
            postItems.Add("returnStationList", true);
            postItems.Add("includeStationArtUrl", true);
            postItems.Add("stationArtSize", "W176H220");
            postItems.Add("returnGenreStations", true);
        }

        /// <summary>
        /// Get the user from the Pandora services response
        /// </summary>
        /// <param name="result">The services response to a login</param>
        private void setUserFromResult(String result)
        {
            Hashtable table = (Hashtable)((Hashtable)JSON.JsonDecode(result))["result"];
            myData = new UserData(table);
            if (table.ContainsKey("stationListResult"))
                getStationsFromLoginResult((Hashtable)table["stationListResult"]);
            if (table.ContainsKey("genreStationsResult"))
                getGenreStationsFromLoginResult((Hashtable)table["genreStationsResult"]);
        }

        /// <summary>
        /// Get the stations associated with the account from the users login
        /// </summary>
        /// <param name="json">The hashtable of json params</param>
        private void getStationsFromLoginResult(Hashtable json)
        {
            ArrayList table = (ArrayList)json["stations"];
            stationsChecksum = (String)json["checksum"];
            for (int i = 0; i < table.Count; ++i)
                myStations.Add(new StationData((Hashtable)table[i]));
        }

        /// <summary>
        /// Get the genre stations associated with the account from the users login
        /// </summary>
        /// <param name="json">The hashtable of json params</param>
        private void getGenreStationsFromLoginResult(Hashtable json)
        {
            ArrayList table = (ArrayList)json["categories"];
            genreStationsChecksum = (String)json["checksum"];
            for (int i = 0; i < table.Count; ++i)
                myGenreStations.Add(new GenreData((Hashtable)table[i]));
        }

        /// <summary>
        /// Decode a string of hex characters into a byte array
        /// </summary>
        /// <param name="paramArrayOfChar">The string of hex</param>
        /// <returns>Byte array representation of the hex</returns>
        private byte[] decodeHex(string paramArrayOfChar)
        {
            int i = paramArrayOfChar.Length;
            if ((i & 0x1) != 0)
            { }
            byte[] arrayOfByte = new byte[i >> 1];
            int j = 0;
            int k = 0;
            while (k < i)
            {
                int m = Convert.ToInt32(paramArrayOfChar[k].ToString(), 16) << 4;
                int n = k + 1;
                int i1 = m | Convert.ToInt32(paramArrayOfChar[n].ToString(), 16);
                k = n + 1;
                arrayOfByte[j] = (byte)(i1 & 0xFF);
                j++;
            }
            return arrayOfByte;
        }
    }
}
